﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
//using CodeGenerationPipeline.CodeGenerationPipelineServiceReference;
using AbstraX.ServerInterfaces;
using AbstraX.Bindings;
using CodeGenerationPipeline.Contracts;
using System.Xml.Schema;
using System.Reflection;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Web;
using AbstraX.Contracts;
using ImpromptuInterface;
using ImpromptuInterface.Dynamic;
using Microsoft.Practices.Unity;
using AbstraX.TypeMappings;
using AbstraX.Contracts;
using Microsoft.Build.Framework;
using DataBindings;
using AbstraX;

namespace CodeGenerationPipeline
{
    public class PipelineService : IPipelineService
    {
        private Dictionary<string, IElementBuild> elementBuilds;
        private XmlSchema codeGeneratorInterfaceSchema;
        private IDomainHostApplication host;
        public event OnOutput OnOutput;

        private class ProcessWatchInfo
        {
            public IGenerator Generator { get; set; }
            public Process Process { get; set; }
            public ManualResetEvent ProcessExitEvent { get; set; }
        }

        public PipelineService(IDomainHostApplication host)
        {
            this.host = host;
            elementBuilds = new Dictionary<string, IElementBuild>();

            RegisterSteps();
        }

        private void RegisterSteps()
        {
            var path = new DirectoryInfo(HttpContext.Current.Request.PhysicalApplicationPath).Parent.FullName;
            var _null = (object)null;

            // todo - come from database

            var step = new
            {
                StepAssemblyFile = Path.Combine(path, @"CodeGenerationPipeline\PipelineGenerators\XmlSchemas\bin\Debug\XmlSchemas.dll"),
                ProjectStructure = _null

            }.ActLike<IPipelineStep>();

            host.PipelineContainer.RegisterInstance<IPipelineStep>("XmlSchemasStep", step);
        }

        public XmlSchema CodeGeneratorInterfaceSchema
        {
            get
            {
                if (codeGeneratorInterfaceSchema == null)
                {
                    var resourceStream = Assembly.GetAssembly(this.GetType()).GetManifestResourceStream(@"CodeGenerationPipeline.CodeGeneratorInterface.xsd");

                    codeGeneratorInterfaceSchema = XmlSchema.Read(resourceStream, OnValidationEvent);
                }

                return codeGeneratorInterfaceSchema;
            }
        }

        private void OnValidationEvent(object sender, ValidationEventArgs e)
        {
            Debugger.Break();
        }

        public void GenerateMiddleLayer(string id)
        {
            var daemon = new DataBindingsDaemon(host);

            host.ClearQueue();
            host.PostEventPercentComplete(25);
            host.PostEventShowUI("DataBindings.dll", id, daemon, new TimeSpan(0, 15, 0));

            if (daemon.BindingsTrees.Count > 0)
            {
                this.GenerateFrom(daemon.BindingsTrees);
            }
        }

        public void GenerateFrom(List<IBindingsTree> bindings)
        {
            var package = (ICodeGenerationPackage)null;
            var directories = new List<DirectoryInfo>();
            var first = true;
            var path = new DirectoryInfo(HttpContext.Current.Request.PhysicalApplicationPath).Parent.FullName;
            var services = bindings.Select(b => b.AbstraXProviderService).Distinct();



            foreach (var service in services)
            {
                var serviceBindings = bindings.Where(b => b.AbstraXProviderService == service);
                var pipelineSteps = new List<IPipelineStep>();

                // ********* hard-coded steps for testing

                var projectRoot = new
                {
                    FindLayer = Return<IProjectStructure>.Arguments<string>((s) => (IProjectStructure)host.Solutions[0].Projects.Where(p => p.Name == "SolutionFramework.Web" && p.ProjectType == "KnownToBeMSBuildFormat").Single())
                }
                .ActLike<IProjectRoot>();

                if (service.InitialPipelineSteps != null)
                {
                    pipelineSteps.AddRange(service.InitialPipelineSteps.Values);
                }

                //pipelineSteps.Add(new
                //{
                //    StepAssemblyFile = Path.Combine(path, @"PipelineGenerators\DomainServices\bin\Debug\Dmb.0omainServices.dll"),
                //    ProjectStructure = host.Solutions[0].Projects.Where(p => p.Name == "SolutionFramework.Service" && p.ProjectType == "KnownToBeMSBuildFormat").Single().Items.Where(i => i.Name == "CodeGenerationPipelineDomainService.cs").Single(i => { i.ProjectRoot = projectRoot; return true; })

                //}.ActLike<IPipelineStep>());

                //pipelineSteps.Add(new
                //{
                //    StepAssemblyFile = Path.Combine(path, @"PipelineGenerators\PresentationServices\bin\Debug\PresentationServices.dll"),
                //    ProjectStructure = host.Solutions[0].Projects.Where(p => p.Name == "CodeGenerationPipeline" && p.ProjectType == "KnownToBeMSBuildFormat").Single().Items.Where(i => i.Name.Replace(@"\", "") == "Services").Single(i => { i.ProjectRoot = projectRoot; return true; })

                //}.ActLike<IPipelineStep>());

                // ********* end testing

                elementBuilds = new Dictionary<string, IElementBuild>();

                AddBindings(bindings);
                ResolveNavigations();

                var rootCount = elementBuilds.Where(e => e.Value.IsRootObject).Count();

                var workspace = Path.Combine(path, @"CodeGenerationPipeline\Workspace\");

                workspace += DateTime.Now.ToString("yyyyMMdd_HHmmss_fffffff");

                foreach (var pipelineStep in pipelineSteps)
                {
                    var assemblyFile = pipelineStep.StepAssemblyFile;
                    var generatorAssembly = Assembly.LoadFrom(assemblyFile);

                    var generatorType = generatorAssembly.GetTypes().Where(t => t.GetInterfaces().Any(i => i.Name == "IGenerator")).Single();
                    var generator = (IGenerator)Activator.CreateInstance(generatorType);
                    var workspacePath = Path.Combine(workspace, generator.Name.Replace(" ", "") + "_" + Guid.NewGuid().ToString().Replace("-", ""));
                    var directoryInfo = new DirectoryInfo(workspacePath);

                    generator.PipelineStep = pipelineStep;

                    if (directoryInfo.Exists)
                    {
                        directoryInfo.GetFiles().ToList().ForEach(f => f.Delete());
                    }
                    else
                    {
                        directoryInfo.Create();
                    }

                    directories.Add(directoryInfo);

                    if (Environment.Is64BitOperatingSystem)
                    {
                        generator.SDKFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86), @"Microsoft SDKs\Windows\v7.0A\Bin\");
                        generator.DotNetFxFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"Microsoft.NET\Framework64\v4.0.30319\");
                    }
                    else
                    {
                        generator.SDKFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), @"Microsoft SDKs\Windows\v7.0A\Bin\");
                        generator.DotNetFxFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), @"Microsoft.NET\Framework\v4.0.30319\");
                    }

                    generator.TemplateEngineHost = new PipelineTemplateEngineHost();
                    generator.OnWatchProcess += new WatchProcess(OnWatchProcess);
                    generator.DomainHostApplication = host;

                    if (first)
                    {
                        package = generator.GenerateFrom(elementBuilds, new DirectoryInfo(workspacePath));
                        first = false;
                    }
                    else
                    {
                        Debug.Assert(package != null);
                        package = generator.GenerateFromPackage(package);
                    }
                }
            }
        }

        private void OnWatchProcess(IGenerator generator, Process process, bool blockUntilFinished, int timeout)
        {
            Thread watchThread = null;

            try
            {
                var exit = false;
                var startTick = Environment.TickCount;
                BuildMessageEventArgs eventArgs;

                var processWatchInfo = new ProcessWatchInfo
                {
                    Generator = generator,
                    Process = process,
                    ProcessExitEvent = new ManualResetEvent(false)
                };

                watchThread = new Thread(o =>
                {
                    var watchInfo = (ProcessWatchInfo)o;

                    while (!exit)
                    {
                        Thread.Sleep(100);

                        if (Environment.TickCount - startTick > timeout)
                        {
                            exit = true;

                            watchInfo.Generator.OnProcessTimeout();
                            processWatchInfo.ProcessExitEvent.Set();

                            break;
                        }
                    }
                });

                process.OutputDataReceived += (s, e) =>
                {
                    var output = e.Data;

                    if (!string.IsNullOrEmpty(output))
                    {
                        eventArgs = new BuildMessageEventArgs(output, process.ProcessName, process.ProcessName, MessageImportance.Normal);

                        OnOutput(this, eventArgs);
                        AppendOutput(generator, output);

                        generator.OnOutput(output);
                    }
                };

                process.ErrorDataReceived += (s, e) =>
                {
                    var error = e.Data;

                    if (!string.IsNullOrEmpty(error))
                    {
                        eventArgs = new BuildMessageEventArgs(error, process.ProcessName, process.ProcessName, MessageImportance.High);

                        OnOutput(this, eventArgs);
                        AppendOutput(generator, error);

                        generator.OnError(0, null, error);
                    }
                };

                process.EnableRaisingEvents = true;
                process.StartInfo.RedirectStandardError = true;
                process.StartInfo.RedirectStandardOutput = true;
                process.StartInfo.UseShellExecute = false;

                process.Exited += (s, e) =>
                {
                    Thread.Sleep(1000);

                    eventArgs = new BuildMessageEventArgs("\r\nProcess exited", process.ProcessName, process.ProcessName, MessageImportance.Normal);

                    OnOutput(this, eventArgs);
                    AppendOutput(generator, "\r\nProcess exited");

                    exit = true;

                    generator.OnProcessExited(process.ExitCode);

                    processWatchInfo.ProcessExitEvent.Set();
                };

                var fileSystemWatcher = new FileSystemWatcher(generator.WorkspaceDirectory);

                fileSystemWatcher.EnableRaisingEvents = true;
                fileSystemWatcher.IncludeSubdirectories = true;

                fileSystemWatcher.Created += (s, e) =>
                {
                    if (e.Name != "Output.log")
                    {
                        generator.OnFileCreated(new FileInfo(e.FullPath));
                    }
                };

                fileSystemWatcher.Changed += (s, e) =>
                {
                    if (e.Name != "Output.log")
                    {
                        generator.OnFileChanged(new FileInfo(e.FullPath));
                    }
                };

                fileSystemWatcher.Renamed += (s, e) =>
                {
                    if (e.Name != "Output.log")
                    {
                        generator.OnFileRenamed(new FileInfo(e.FullPath), new FileInfo(e.OldFullPath));
                    }
                };

                fileSystemWatcher.Deleted += (s, e) =>
                {
                    generator.OnFileDeleted(new FileInfo(e.FullPath));
                };

                fileSystemWatcher.Error += (s, e) =>
                {
                    generator.OnError(0, e.GetException(), "File System Watcher error.");
                };

                eventArgs = new BuildMessageEventArgs(process.StartInfo.FileName + " " + process.StartInfo.Arguments + "\r\n", process.ProcessName, process.ProcessName, MessageImportance.Normal);

                OnOutput(this, eventArgs);
                AppendOutput(generator, process.StartInfo.FileName + " " + process.StartInfo.Arguments + "\r\n");

                watchThread.Start(processWatchInfo);
                process.Start();

                process.BeginErrorReadLine();
                process.BeginOutputReadLine();

                watchThread.Join();

                processWatchInfo.ProcessExitEvent.WaitOne();
            }
            catch (Exception ex)
            {
                if (watchThread != null && watchThread.IsAlive)
                {
                    watchThread.Abort();
                }

                generator.OnError(0, ex, "OnWatchProcess error.");
            }
        }

        private void AppendOutput(IGenerator generator, string output)
        {
            var writer = new StreamWriter(Path.Combine(generator.WorkspaceDirectory, "Output.log"), true);

            writer.WriteLine(output);

            writer.Close();
        }

        public void AddBindings(List<IBindingsTree> bindings)
        {
            foreach (var bindingsTree in bindings)
            {
                AddBindings(bindingsTree);
            }
        }

        protected void ResolveNavigations()
        {
            foreach (var elementBuild in elementBuilds.Values)
            {
                var remove = new List<NavigationItem>();

                foreach (var navigationItem in elementBuild.NavigationItems)
                {
                    if (elementBuilds.ContainsKey(navigationItem.NavigationResultElement.DataType.FullyQualifiedName))
                    {
                        var resultElementBuild = elementBuilds[navigationItem.NavigationResultElement.DataType.FullyQualifiedName];

                        navigationItem.NavigationResultBuild = resultElementBuild;
                    }
                    else
                    {
                        remove.Add(navigationItem);
                    }
                }

                remove.ForEach(n => elementBuild.NavigationItems.Remove(n));
            }
        }

        protected void AddBindings(IBindingsTree bindingsTree)
        {
            foreach (var bindingsTreeNode in bindingsTree.RootBindings)
            {
                AddBindings(bindingsTreeNode, true);
            }
        }

        protected void AddBindings(IBindingsTreeNode bindingNode, bool isRoot)
        {
            var rootAdded = false;

            if (!isRoot)
            {
                rootAdded = true;
            }

            if (bindingNode.PropertyBindings != null)
            {
                foreach (var propertyBinding in bindingNode.PropertyBindings)
                {
                    var element = GetElementForBinding(bindingNode, propertyBinding);
                    IElementBuild elementBuild;

                    if (elementBuilds.ContainsKey(element.DataType.FullyQualifiedName))
                    {
                        elementBuild = elementBuilds[element.DataType.FullyQualifiedName];

                        elementBuild.Bindings.Add(propertyBinding);
                    }
                    else
                    {
                        elementBuild = new ElementBuild();

                        elementBuild.Element = element;
                        elementBuild.IsRootObject = isRoot;
                        elementBuild.Bindings.Add(propertyBinding);

                        rootAdded = true;

                        elementBuilds.Add(element.DataType.FullyQualifiedName, elementBuild);
                    }
                }
            }

            if (bindingNode.ChildNodes != null)
            {
                foreach (var childBinding in bindingNode.ChildNodes)
                {
                    foreach (var childElement in childBinding.GetContextObjects())
                    {
                        object navigationProperty;
                        IElement navigationOwner;

                        GetNavigationPropertyOwner(childBinding, childElement, out navigationOwner, out navigationProperty);

                        if (navigationOwner != null)
                        {
                            IElementBuild elementBuild;

                            if (elementBuilds.ContainsKey(navigationOwner.DataType.FullyQualifiedName))
                            {
                                elementBuild = elementBuilds[navigationOwner.DataType.FullyQualifiedName];

                                if (navigationProperty is string)
                                {
                                    elementBuild.NavigationItems.Add(new NavigationItem((string)navigationProperty, childElement));
                                }
                                else if (navigationProperty is IElement)
                                {
                                    elementBuild.NavigationItems.Add(new NavigationItem((IElement)navigationProperty, childElement));
                                }
                                else
                                {
                                    Debugger.Break();
                                }
                            }
                            else
                            {
                                elementBuild = new ElementBuild();

                                elementBuild.Element = navigationOwner;

                                if (!rootAdded)
                                {
                                    elementBuild.IsRootObject = isRoot;
                                    rootAdded = true;
                                }

                                if (navigationProperty is string)
                                {
                                    elementBuild.NavigationItems.Add(new NavigationItem((string)navigationProperty, childElement));
                                }
                                else if (navigationProperty is IElement)
                                {
                                    elementBuild.NavigationItems.Add(new NavigationItem((IElement)navigationProperty, childElement));
                                }
                                else
                                {
                                    Debugger.Break();
                                }

                                elementBuilds.Add(navigationOwner.DataType.FullyQualifiedName, elementBuild);
                            }
                        }

                        AddBindings(childBinding, rootAdded == false);
                    }
                }
            }
        }

        private void GetNavigationPropertyOwner(IBindingsTreeNode bindingNode, IElement childElement, out IElement owner, out object property)
        {
            var context = bindingNode.ParentNode.DataContext.Where(e => e.ContextObject.ID == bindingNode.ParentSourceElement.ID).Single();

            if (context != null)
            {
                if (context.ContainerType == ContainerType.Property)
                {
                    owner = bindingNode.ParentNode.ParentSourceElement;
                    property = context.ContextObject;
                }
                else
                {
                    owner = context.ContextObject;
                    property = childElement.Name;
                }
            }
            else
            {
                throw new Exception("");
            }
        }

        private IElement GetElementForBinding(IBindingsTreeNode bindingNode, IPropertyBinding binding)
        {
            var context = bindingNode.DataContext.FirstOrDefault(e => binding.BindingSource is IAbstraXBindingSource ? 
                e.ContextObject.ID == ((IAbstraXBindingSource) binding.BindingSource).BindingAttribute.ParentID 
                : false /* TODO */);

            if (context != null)
            {
                return context.ContextObject;
            }
            else
            {
                return GetElementForBinding(bindingNode.ParentNode, binding);
            }
        }
    }
}
